import Solve from './solve'
import type { UnionFunc } from './solve'

type UnknownPosition = 1 | 2
class BisectionalMonotonic extends Solve {
  constructor(unionFunc: UnionFunc) {
    super(unionFunc)
  }
  unarySolve(target: number, initialGuess: [number, number]): number {
    if (!this.originalUnaryFunc) {
      throw new Error('originalUnaryFunc is not defined')
    }
    return this.bisectionMethod(target, initialGuess, (x) =>
      this.originalUnaryFunc!(x),
    )
  }
  binarySolve(
    target: number,
    knownVal: number,
    initialGuess: [number, number],
    unknownPosition: UnknownPosition = 1,
  ): number {
    if (!this.originalBinaryFunc) {
      throw new Error('originalBinaryFunc is not defined')
    }
    return this.bisectionMethod(target, initialGuess, (x) =>
      unknownPosition === 1
        ? this.originalBinaryFunc!(x, knownVal)
        : this.originalBinaryFunc!(knownVal, x),
    )
  }

  ternarySolve(
    target: number,
    knownVals: [number, number],
    initialGuess: [number, number],
    unknownPosition: 1 | 2 | 3 = 1,
  ): number {
    if (!this.originalTernaryFunc) {
      throw new Error('originalTernaryFunc is not defined')
    }
    return this.bisectionMethod(target, initialGuess, (x) =>
      unknownPosition === 1
        ? this.originalTernaryFunc!(x, knownVals[0], knownVals[1])
        : unknownPosition === 2
          ? this.originalTernaryFunc!(knownVals[0], x, knownVals[1])
          : this.originalTernaryFunc!(knownVals[0], knownVals[1], x),
    )
  }

  private bisectionMethod(
    target: number,
    initialGuess: [number, number],
    evaluateFunc: (x: number) => number,
  ): number {
    let [l, r] = initialGuess
    if (l > r) [l, r] = [r, l]

    let fl = evaluateFunc(l) - target,
      fr = evaluateFunc(r) - target

    if (fl * fr > 0) {
      throw new Error('The function does not change sign in the given interval')
    }

    for (let i = 0; i < Solve.MAX_ITERATIONS; i++) {
      const mid = r - (r - l) / 2
      const fMid = evaluateFunc(mid) - target

      if (
        Math.abs(fMid) <= Solve.TOLERANCE ||
        Math.abs(l - r) < Solve.TOLERANCE
      ) {
        return mid
      }

      if (fMid * fl < 0) {
        r = mid
        fr = fMid
      } else {
        l = mid
        fl = fMid
      }
    }
    throw new Error('Failed to find a solution within the given tolerance')
  }
}

export default BisectionalMonotonic
